package com.jerry.service.Impl;

import com.jerry.BO.ServerInfo;
import com.jerry.DO.FileChunkMeta;
import com.jerry.DO.MetaFile;
import com.jerry.DTO.CompleteChunkFileDTO;
import com.jerry.DTO.FileMeta;
import com.jerry.config.MetaConfig;
import com.jerry.errors.BusinessException;
import com.jerry.errors.EnumException;
import com.jerry.generator.FilenameGenerator;
import com.jerry.selector.ServerSelector;
import com.jerry.service.DiscoveryService;
import com.jerry.service.MetaService;
import com.jerry.utils.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class MetaServiceImpl implements MetaService {
    private final HttpServletRequest request;
    private final DiscoveryService discoveryService;
    private final FilenameGenerator filenameGenerator;
    private final ServerSelector serverSelector;
    private final MongoTemplate mongoTemplate;
    private final MetaConfig metaConfig;

    public MetaServiceImpl(HttpServletRequest request,
                           DiscoveryService discoveryService,
                           FilenameGenerator filenameGenerator,
                           ServerSelector serverSelector,
                           MongoTemplate mongoTemplate,
                           MetaConfig metaConfig) {
        this.request = request;
        this.discoveryService = discoveryService;
        this.filenameGenerator = filenameGenerator;
        this.serverSelector = serverSelector;
        this.mongoTemplate = mongoTemplate;
        this.metaConfig = metaConfig;
    }

    @Override
    public MetaFile generate(FileMeta fileMeta) {
        String bucketName = fileMeta.getBucketName();
        String extension = fileMeta.getExtension();
        Long fileSize = fileMeta.getFileSize();
        String nickName = fileMeta.getNickName();
        String fileType = fileMeta.getFileType();
        String ipAddr = RequestUtil.getIpAddr(request);
        String base64 = fileMeta.getBase64();
        Integer chunkSize = metaConfig.getChunkSize();
        String filename = filenameGenerator.generate(fileMeta, ipAddr);
        MetaFile metaFile = mongoTemplate.findById(filename, MetaFile.class);
        if (metaFile != null) {
            return metaFile;
        }
        log.info("{}", chunkSize);
        int totalchunks = (int) Math.ceil(fileSize * 1.0 / chunkSize);
        List<FileChunkMeta> chunks = createChunks(fileSize, extension, bucketName, chunkSize, filename, totalchunks);
        metaFile = new MetaFile();
        metaFile.setBucketName(bucketName)
                .setExtension(extension)
                .setFileName(filename)
                .setFileSize(fileSize)
                .setTotalChunk(totalchunks)
                .setFileChunkMetas(chunks)
                .setNickName(nickName)
                .setFileType(fileType)
                .setBase64(base64)
                .setUploadTime(LocalDateTime.now());
        mongoTemplate.insert(metaFile);
        return metaFile;
    }

    @Override
    public void complteteChunk(CompleteChunkFileDTO completeChunkFileDTO) {
        String filename = completeChunkFileDTO.getFilename();
        MetaFile metaFile = mongoTemplate.findById(filename, MetaFile.class);
        log.info(metaFile.getFileChunkMetas().toString());
        if (metaFile == null) {
            throw new RuntimeException("找不到Metafile");
        }
        metaFile.getFileChunkMetas().forEach(c -> {
            if (c.getChunkNo().equals(completeChunkFileDTO.getChunkNo()) &&
                    c.getAddress().equals(completeChunkFileDTO.getAddress()) &&
                    c.getSchema().equals(completeChunkFileDTO.getSchema())) {
                c.setChunkMd5(completeChunkFileDTO.getMd5());
                c.setCompleted(true);
            }
        });
        mongoTemplate.save(metaFile);
    }

    @Override
    public MetaFile meta(String bucketName, String filename) {
        MetaFile metaFile = mongoTemplate.findById(filename, MetaFile.class);
        if (metaFile == null) {
            throw new BusinessException(EnumException.META_FILE_NOT_FOUND);
        }
        Set<String> addressSet = discoveryService.aliveServers()
                .stream()
                .map(serverInfo -> serverInfo.getHost() + ":" + serverInfo.getPort())
                .collect(Collectors.toSet());
        List<FileChunkMeta> chunks = metaFile.getFileChunkMetas();
        for (FileChunkMeta chunk : chunks) {
            if (!addressSet.contains(chunk.getAddress())) {
                chunk.setWeight(chunk.getWeight() > 0 ? chunk.getWeight() - 1 : 0);
            } else {
                chunk.setWeight(metaConfig.getChunkInstanceMaxWeight());
            }
            if (!chunk.getCompleted()) {
                throw new BusinessException(EnumException.CHUNK_FILE_NOT_UPLOADED);
            }
        }
        mongoTemplate.save(metaFile);
        chunks = chunks.stream()
                .collect(Collectors.groupingBy(FileChunkMeta::getChunkNo))
                .values()
                .stream()
                .parallel()
                .map(fileChunkMetas -> fileChunkMetas.stream()
                        .peek(fileChunkMeta -> {
                            if (!addressSet.contains(fileChunkMeta.getAddress())) {
                                fileChunkMeta.setWeight(fileChunkMeta.getWeight() > 0 ? fileChunkMeta.getWeight() - 1 : 0);
                            }
                        })
                        .max(Comparator.comparing(FileChunkMeta::getWeight))
                        .orElse(new FileChunkMeta())
                )
                .filter(fileChunkMeta -> fileChunkMeta.getWeight() > 0)
                .collect(Collectors.toList());
        if (chunks.size() < metaFile.getTotalChunk()){
            Set<Integer> chunkNoSet = chunks.stream().map(FileChunkMeta::getChunkNo).collect(Collectors.toSet());
            List<Integer> lossChunkNo = new ArrayList<>();
            for (int i = 0; i < metaFile.getTotalChunk(); i++) {
                if (chunkNoSet.contains(i)){
                    lossChunkNo.add(i);
                }
            }
            log.info("文件 {} 存在分片不可用现象: {}", filename, lossChunkNo);
            throw new BusinessException(EnumException.NO_CHUNK_META_AVAILABLE);
        }
        metaFile.setFileChunkMetas(chunks);
        return metaFile;
    }

    @Override
    public List<MetaFile> files(String filetype) {
        List<MetaFile> metaFiles = mongoTemplate.findAll(MetaFile.class);
        return metaFiles.stream().filter(metaFile -> metaFile.getFileType().equals(filetype)).collect(Collectors.toList());
    }

    @Override
    public List<FileChunkMeta> chunkInfo(String bucketName, String fileName, Integer chunkNo) {
        if (fileName.contains(".")){
            fileName = fileName.split("\\.")[0];
        }
        MetaFile metaFile = mongoTemplate.findById(fileName, MetaFile.class);
        if (Objects.isNull(metaFile)){
            throw new BusinessException(EnumException.META_FILE_NOT_FOUND);
        }
        return metaFile.getFileChunkMetas()
                .stream()
                .filter(chunk -> chunk.getChunkNo().equals(chunkNo))
                .collect(Collectors.toList());
    }

    @Override
    public void delete(String bucketName, String fileName) {
        Query query = new Query(Criteria.where("bucketName").is(bucketName))
                .addCriteria(Criteria.where("fileName").is(fileName));
        mongoTemplate.remove(query,MetaFile.class);
    }

    public List<FileChunkMeta> createChunks(Long filesize,
                                            String extension,
                                            String bucketName,
                                            Integer chunkSize,
                                            String filename,
                                            int totalChunk) {
        List<FileChunkMeta> chunks = new ArrayList<>();
        long start = 0;
        for (int i = 0; i < totalChunk; i++) {
            long concurrentChunksize = chunkSize;
            if (filesize < (long) (i + 1) * chunkSize) {
                concurrentChunksize = filesize - (long) i * chunkSize;
            }
            List<ServerInfo> aliveServers = discoveryService.aliveServers();
            List<ServerInfo> selectedServers = serverSelector.select(aliveServers, metaConfig.getChunkInstanceCount());
            for (ServerInfo selectedServer : selectedServers) {
                FileChunkMeta fileChunkMeta = new FileChunkMeta();
                String address = selectedServer.getHost() + ":" +
                        selectedServer.getPort();
                fileChunkMeta.setBucketName(bucketName)
                        .setChunkNo(i)
                        .setChunkSize((int) concurrentChunksize)
                        .setFilename(filename)
                        .setAddress(address)
                        .setChunkStart(start)
                        .setExtension(extension)
                        .setSchema("http")
                        .setWeight(metaConfig.getChunkInstanceMaxWeight());
                chunks.add(fileChunkMeta);
            }
            start += concurrentChunksize;
        }
        return chunks;
    }
}
